package com.ndp.fb.fbagent.service.jsonmapper;

import com.ndp.fb.annotation.FBParam;
import com.restfb.DefaultJsonMapper;
import com.restfb.exception.FacebookJsonMappingException;
import com.restfb.json.JsonArray;
import com.restfb.json.JsonException;
import com.restfb.json.JsonObject;
import com.restfb.util.ReflectionUtils;
import com.restfb.util.StringUtils;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.restfb.json.JsonObject.NULL;
import static com.restfb.util.ReflectionUtils.findFieldsWithAnnotation;
import static com.restfb.util.ReflectionUtils.isPrimitive;
import static java.lang.String.format;
import static java.util.Collections.unmodifiableSet;

/**
 * Created by infi on 2015/3/13.
 */
public class FbParamJsonMapper extends DefaultJsonMapper {
    private org.slf4j.Logger  logger = LoggerFactory.getLogger(FbParamJsonMapper.class);
    @Override
    protected Object toJsonInternal(Object object, boolean ignoreNullValuedProperties) {
        if (object == null)
            return NULL;

        if (object instanceof List<?>) {
            JsonArray jsonArray = new JsonArray();
            for (Object o : (List<?>) object)
                jsonArray.put(toJsonInternal(o, ignoreNullValuedProperties));

            return jsonArray;
        }

        if (object instanceof Map<?, ?>) {
            JsonObject jsonObject = new JsonObject();
            for (Map.Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) {
                if (!(entry.getKey() instanceof String))
                    throw new FacebookJsonMappingException("Your Map keys must be of type " + String.class
                            + " in order to be converted to JSON.  Offending map is " + object);

                try {
                    jsonObject.put((String) entry.getKey(), toJsonInternal(entry.getValue(), ignoreNullValuedProperties));
                } catch (JsonException e) {
                    throw new FacebookJsonMappingException("Unable to process value '" + entry.getValue() + "' for key '"
                            + entry.getKey() + "' in Map " + object, e);
                }
            }

            return jsonObject;
        }

        if (isPrimitive(object))
            return object;

        if (object instanceof BigInteger)
            return ((BigInteger) object).longValue();

        if (object instanceof BigDecimal)
            return ((BigDecimal) object).doubleValue();

        // We've passed the special-case bits, so let's try to marshal this as a
        // plain old Javabean...

        List<ReflectionUtils.FieldWithAnnotation<FBParam>> fieldsWithAnnotation =
                findFieldsWithAnnotation(object.getClass(), FBParam.class);

        JsonObject jsonObject = new JsonObject();

        // No longer throw an exception in this case. If there are multiple fields
        // with the same @Facebook value, it's luck of the draw which is picked for
        // JSON marshaling.
        // TODO: A better implementation would query each duplicate-mapped field. If
        // it has is a non-null value and the other duplicate values are null, use
        // the non-null field.
        Set<String> facebookFieldNamesWithMultipleMappings = facebookFieldNamesWithMultipleMappings1(fieldsWithAnnotation);
        if (facebookFieldNamesWithMultipleMappings.size() > 0 ){
            logger.warn(format("Unable to convert to JSON because multiple @" + FBParam.class.getSimpleName()
                    + " annotations for the same name are present: " + facebookFieldNamesWithMultipleMappings));
        }


        for (ReflectionUtils.FieldWithAnnotation<FBParam> fieldWithAnnotation : fieldsWithAnnotation) {
            String facebookFieldName = getFacebookFieldName(fieldWithAnnotation);
            fieldWithAnnotation.getField().setAccessible(true);

            try {
                Object fieldValue = fieldWithAnnotation.getField().get(object);

                if (!(ignoreNullValuedProperties && fieldValue == null))
                    jsonObject.put(facebookFieldName, toJsonInternal(fieldValue, ignoreNullValuedProperties));
            } catch (Exception e) {
                throw new FacebookJsonMappingException("Unable to process field '" + facebookFieldName + "' for "
                        + object.getClass(), e);
            }
        }

        return jsonObject;
    }

    protected String getFacebookFieldName(ReflectionUtils.FieldWithAnnotation fieldWithAnnotation) {
        String facebookFieldName = ((FBParam)fieldWithAnnotation.getAnnotation()).value();
        Field field = fieldWithAnnotation.getField();
        if(StringUtils.isBlank(facebookFieldName)) {
            if(logger.isDebugEnabled()) {
                logger.debug("No explicit Facebook field name found for " + field + ", so defaulting to the field name itself (" + field.getName() + ")");
            }

            facebookFieldName = field.getName();
        }

        return facebookFieldName;
    }

    protected Set<String> facebookFieldNamesWithMultipleMappings1(List<ReflectionUtils.FieldWithAnnotation<FBParam>> fieldsWithAnnotation) {
        Map<String, Integer> facebookFieldsNamesWithOccurrenceCount = new HashMap<String, Integer>();
        Set<String> facebookFieldNamesWithMultipleMappings = new HashSet<String>();

        // Get a count of Facebook field name occurrences for each
        // @Facebook-annotated field
        for (ReflectionUtils.FieldWithAnnotation<FBParam> fieldWithAnnotation : fieldsWithAnnotation) {
            String fieldName = getFacebookFieldName(fieldWithAnnotation);
            int occurrenceCount =
                    facebookFieldsNamesWithOccurrenceCount.containsKey(fieldName) ? facebookFieldsNamesWithOccurrenceCount
                            .get(fieldName) : 0;
            facebookFieldsNamesWithOccurrenceCount.put(fieldName, occurrenceCount + 1);
        }

        // Pull out only those field names with multiple mappings
        for (Map.Entry<String, Integer> entry : facebookFieldsNamesWithOccurrenceCount.entrySet())
            if (entry.getValue() > 1)
                facebookFieldNamesWithMultipleMappings.add(entry.getKey());

        return unmodifiableSet(facebookFieldNamesWithMultipleMappings);
    }
}
